Claude Sonnet
Différentes version de Claude Sonnet :
Journaux liées à cette note :
J'ai découvert les types "unknown" et "never" en TypeScript
En TypeScript, dans mon projet professionnel, #JaiDécouvert le type unknown qui ressemble à any mais qui est différent.
Exemple (produit par Claude Sonnet 4.5) avec any :
let value: any;
value.foo.bar(); // No error, even if it crashes at runtime
value.trim(); // No error, even if value is a number
Exemple avec unknown :
let value: unknown;
value.trim(); // Error: Object is of type 'unknown'
// You must narrow the type first
if (typeof value === 'string') {
value.trim(); // OK, TypeScript knows it's a string
}
unknown a été introduit dans la version 3.0 de TypeScript en 2018 : Announcing TypeScript 3.0 - The unknown type.
J'ai trouvé les réponses à cette question StackOverflow intéressantes : 'unknown' vs. 'any'.
C'est peut-être parce que je ne suis pas habitué à la documentation de TypeScript , mais j'ai l'impression que la fonctionnalité unknown n'est pas correctement documentée. Par exemple, je suis surpris de trouver presque rien à son sujet dans la page Everyday-types , ni dans les chapitres "Reference" :

Et rien non plus dans les tutoriels.
Au passage, j'ai aussi découvert le type never.
#JaimeraisUnJour prendre le temps de parcourir la documentation de TypeScript de manière exhaustive. Jusqu'à présent, je n'en ai jamais eu réellement besoin, car je n'ai jamais contribué à de projet écrit en TypeScript. Mais maintenant, cela devient une nécessité pour mon projet professionnel.
Fusion de CoreOS et Atomic Project en 2018
Cette note fait partie de la série de notes : "J'ai étudié et testé CoreOS et je suis tombé dans un rabbit hole 🙈".
Note précédente : "2014-2018 approche alternative avec Atomic Project".
Suite au rachat de la société CoreOS par Red Hat en 2018, les projets CoreOS Container Linux et Fedora Atomic Host ont fusionné en juillet 2019 pour donner Fedora CoreOS.
D'après mon analyse, mise à part ignition, le projet Fedora CoreOS est construit sur les bases de Fedora Atomic Host et n'a gardé de CoreOS Container Linux que le nom "CoreOS".
Cette nouvelle distribution Fedora CoreOS reste atomic et immutable comme l'ancien CoreOS Container Linux, mais utilise désormais rpm-ostree et OSTree (au lieu du système dual partition A/B), et permet le package layering si nécessaire. La philosophie "100% conteneurs" reste encouragée, mais n'est plus une contrainte absolue.
Voici une chronologie sur l'histoire de CoreOS que m'a proposée Claude Sonnet 4.5 :
2013-2017: CoreOS Container Linux
├─ Atomic ✓ (dual partition)
├─ Immutable ✓
└─ Package layering ✗
2014-2018: Fedora/RHEL Atomic Host
├─ Atomic ✓ (OSTree)
├─ Immutable ✓
└─ Package layering ✓ (rpm-ostree)
2018: Rachat CoreOS par Red Hat
2019+: Fedora CoreOS (fusion des deux)
├─ Atomic ✓ (OSTree)
├─ Immutable ✓
├─ Package layering ✓ (possible mais découragé)
└─ Philosophie: conteneurs first, mais flexible
Note suivante : "Quelques outils CoreOS : coreos-installer, graphe de migration et zincati".
Je fais mon retour dans l'écosystème React, j'ai découvert Jotai et Zustand
Dans le code source de mon projet professionnel, #JaiDécouvert la librairie ReactJS nommée Jotai (https://jotai.org).
Les atom de Jotai ressemblent aux fonctionnalités Svelte Store. Jotai permet entre autres d'éviter de faire du props drilling.
Pour en savoir plus sur l'intérêt de Jotai versus "React context (useContext + useState)", je vous conseille la lecture d'introduction de la page Comparison de la documentation Jotai. J'ai trouvé la section "Usage difference" très simple à comprendre.
Cette découverte est une bonne surprise pour moi, car les atom de Jotai reproduisent l'élégance syntaxique des Store de Svelte, ce qui améliore mon confort de développement en ReactJS. #JaiLu ce thread Hacker News en lien avec le sujet : "I like Svelte more than React (it's store management)".
Je tiens toutefois à préciser que si Jotai améliore significativement mon expérience de développeur (DX) avec ReactJS, cela reste une solution de gestion d'état au sein du runtime ReactJS. En comparaison, le compilateur Svelte génère du code optimisé natif qui reste intrinsèquement plus performant à l'exécution.
Exemple Svelte :
import { writable, derived } from 'svelte/store';
const count = writable(0);
const doubled = derived(count, $count => $count * 2);
// Usage dans component
$count // auto-subscription
Exemple ReactJS basé sur Jotai :
import { atom } from 'jotai';
const countAtom = atom(0);
const doubledAtom = atom(get => get(countAtom) * 2);
// Usage dans component
const [count] = useAtom(countAtom);
J'ai lu la page "Comparison" de Jotai pour mieux comprendre la place qu'a Jotai dans l'écosystème ReactJS.
#JaiDécouvert deux autres librairies développées par la même personne, Daishi Kato : Zustand et Valtio. D'après ce que j'ai compris, Daishi a développé ces librairies dans cet ordre :
- Zustand en juin 2019 - voir "How Zustand Was Born"
- La première version de Jotai en septembre 2020 - voir "How Jotai Was Born"
- La première version de Valtio en mars 2021 - voir "How Valtio Was Born"
J'ai aussi découvert Recoil développé par Facebook, mais d'après son entête GitHub celle-ci semble abandonnée. Une migration de Recoil vers Jotai semble être conseillée.
J'aime beaucoup comment Daishi Kato choisit le nom de ses librairies, la méthode est plutôt simple 🙂 :
Comme mentionné plus haut, Jotai ressemble à Recoil alors que Zustand ressemble à Redux :
Analogy
Jotai is like Recoil. Zustand is like Redux.
...
How to structure state
Jotai state consists of atoms (i.e. bottom-up). Zustand state is one object (i.e. top-down).
Même en lisant la documentation Comparison, j'ai eu de grandes difficulté à comprendre quand préférer Zustand à Jotai.
En lisant la documentation, Jotai me semble toujours plus simple à utiliser que Zustand.
Avec l'aide de Claude Sonnet 4.5, je pense avoir compris quand préférer Zustand à Jotai.
Exemple Zustand
Dans l'exemple Zustand suivant, la fonction addToCart modifie plusieurs parties du state useCartStore en une seule transaction :
import { create } from 'zustand'
const useCartStore = create((set) => ({
user: null,
cart: [],
notifications: [],
addToCart: (product) => set((state) => {
return {
cart: [...state.cart, product],
notifications: (
state.user
? [...state.notifications, { type: 'cart_updated' }]
: state.notifications
)
};
};
}));
Et voici un exemple d'utilisation de addToCart dans un composant :
function ProductCard({ product }) {
// Sélectionner uniquement l'action addToCart
const addToCart = useCartStore((state) => state.addToCart);
return (
<div>
<h3>{product.name}</h3>
<p>{product.price}€</p>
<button onClick={() => addToCart(product)}>
Ajouter au panier
</button>
</div>
);
}
Exemple Jotai
Voici une implémentation équivalente basée sur Jotai :
import { atom } from 'jotai';
const userAtom = atom(null);
const cartAtom = atom([]);
const notificationsAtom = atom([]);
export const addToCartAtom = atom(
null,
(get, set, product) => {
const user = get(userAtom);
const cart = get(cartAtom);
const notifications = get(notificationsAtom);
set(cartAtom, [...cart, product]);
if (user) {
set(notificationsAtom, [...notifications, { type: 'cart_updated' }]);
}
}
);
Et voici un exemple d'utilisation de useToCartAtom dans un composant :
import { useSetAtom } from 'jotai';
import { addToCartAtom } from 'addToCartAtom';
function ProductCard({ product }) {
// Récupérer uniquement l'action (pas la valeur)
const addToCart = useSetAtom(addToCartAtom);
return (
<div>
<h3>{product.name}</h3>
<p>{product.price}€</p>
<button onClick={() => addToCart(product)}>
Ajouter au panier
</button>
</div>
);
}
Ces deux exemples montrent que Zustand est plus élégant et probablement plus performant que Jotai pour gérer des actions qui conditionnent ou modifient plusieurs parties du state simultanément.
#JaiLu le thread SubReddit ReactJS "What do you use for global state management? " et j'ai remarqué que Zustand semble plutôt populaire.
En rédigeant cette note, j'ai découvert Valtio qui semble être une alternative à MobX. Je prévois d'étudier ces deux librairies dans une future note.
Système de mise à jour d'Android, Chrome OS, MacOS et MS Windows
Cette note fait partie de la série de notes : "J'ai étudié et testé CoreOS et je suis tombé dans un rabbit hole 🙈".
Note précédente : "Ajout de packages dans des distributions atomiques".
Chrome OS et Android implémentent la stratégie de double partition A/B (seamless) system updates.
Cette technologie offre des mises à jour complètement transparentes en arrière-plan et un redémarrage immédiat.
En revanche, contrairement à la solution CoreOS (méthode détaillée dans cette note), cette méthode a pour inconvénient de consommer deux fois plus d'espace de stockage.
MacOS s'appuie sur les snapshots de son filesystem APFS (fonctionnalité qu'offre aussi btrfs). Cela garantit un retour en arrière rapide vers la version antérieure si des problèmes surviennent.
En revanche, l'upgrade se termine durant le reboot, pouvant prendre de 2 à 5 minutes, alors que le redémarrage reste instantané avec Chrome OS, Android, CoreOS ou Fedora Silverblue.
Comme d'habitude, je n'arrive pas à trouver des informations précises sur le fonctionnement interne de MS Windows 😔. D'après Claude Sonnet 4, le système de mise à jour de Windows 10 et Windows 11, baptisé Unified Update Platform (UUP), semble plutôt daté : pas d'A/B (seamless) system updates, absence d'atomicité, installation longue lors du reboot (10 à 30 minutes), possibilité d'échec en cours de processus, rollback complexe, aucun système de snapshot comparable à MacOS. J'ai du mal à croire ce bilan tellement catastrophique, ce qui m'amène à questionner sur l'exactitude des informations rapportées par Claude Sonnet 4.
D'après cette documentation particulièrement riche et mes recherches complémentaires, je pense que la stack libostree + composefs (avec zstd:chunked ) tel qu'implémenté dans Fedora CoreOS est probablement la technologie de mise à jour la plus avancée actuellement disponible.
Avant de présenter le fonctionnement du système de mise à jour de Fedora CoreOS en 2025, je vais retracer l'évolution technique de cette solution.
Note suivante : "CoreOS de 2013 à 2018".
Publication du projet 33 - "POC serveur Git HTTP qui injecte du contenu dans OpenSearch"
Je viens de terminer le "Projet 33 - "POC serveur Git HTTP qui injecte du contenu dans OpenSearch"" en 25h.
Si j'inclus le travail préliminaire du Projet 32 - "POC serveur Git HTTP avec exécution de scripts au push", cela représente 34h au total.
Voici le repository avec le résultat final : https://github.com/stephane-klein/poc-content-repository-git-to-opensearch.
J'ai réussi à implémenter preque tous les éléments que j'avais prévu :
- Un serveur Git HTTP supportant les opérations push et pull
- Après chaque git push, injection automatique des données reçues vers une base de données OpenSearch
- Intégration d'un système de job queue minimaliste qui permet de traiter les tâches d'importation des données Git vers OpenSearch de manière asynchrone. Cela permet entre autres de rendre l'opération git push non bloquante.
- Le modèle de données doit permettre l'accès au contenu de plusieurs branches.
- Upload des fichiers binaires vers un serveur Minio tout concervant leurs metadata (chemin, branche, etc) dans OpenSearch.
- La suppression d'une branche ou d'un commit doit aussi supprimer les données présentes dans OpenSearch et Minio.
- Utilisation de la librairie nodegit.
Le seul élément que je n'ai pas testé est celui-ci :
- L'accès aux données via l'API de OpenSearch ne doit pas être perturbé pendant les phases d'importation de données depuis Git.
Je précise d'emblée que l'implémentation de la fonctionnalité d'exploration web du content repository manque actuellement d'élégance.
Les dossiers suivants contiennent une quantité importante de code dupliqué :
src/routes/[...pathname]/,src/routes/branches/[branch_name]/[...pathname]/- et
src/routes/r/[revision]/[...pathname]/
src/routes
├── branches
│ ├── [branch_name]
│ │ ├── history
│ │ │ ├── +page.server.js
│ │ │ └── +page.svelte
│ │ ├── +page.server.js
│ │ ├── +page.svelte
│ │ └── [...pathname]
│ │ ├── +page.server.js
│ │ └── +page.svelte
│ ├── +page.server.js
│ └── +page.svelte
├── +page.server.js
├── +page.svelte
├── [...pathname]
│ ├── +page.server.js
│ ├── +page.svelte
│ └── raw
│ └── +server.js
└── r
├── +page.server.js
└── [revision]
├── history
│ ├── +page.server.js
│ └── +page.svelte
├── +page.server.js
├── +page.svelte
└── [...pathname]
├── +page.server.js
├── +page.svelte
└── raw
Pour le moment, je n'ai pas encore trouvé comment éviter cette duplication de manière élégante.
J'ai pensé à 3 approches pour améliorer cette implémentation :
- Factoriser la logique de query des fichiers
+page.server.jsdans une fonction partagée. - Migrer complètement ces pages d'exploration vers
src/hooks.server.js(avec les Server hooks de SvelteKit ).
Comme cette partie n'était pas au cœur du projet, j'ai préféré ne pas y investir davantage de temps.
Dans ce projet, j'ai utilisé pour la première fois OpenSearch, le fork de Elasticsearch. J'ai dû faire quelques adaptations par rapport à Elasticsearch mais rien de vraiment complexe.
J'ai utilisé la librairie @opensearch-project/opensearch avec succès, bien aidé par Claude Sonnet 4 pour écrire mes query OpenSearch.
J'aimerais mieux maîtriser l'api de OpenSearch et Elasticsearch, mais je ne les utilise pas suffisamment.
Cette dépendance à un LLM pour écrire ces requêtes me contrarie, je me sens prolétaire et j'ai le sentiment de perdre l'habitude de l'effort. Je pense à cette recherche "Your Brain on ChatGPT: Accumulation of Cognitive Debt when Using an AI Assistant for Essay Writing Task" et cela me préoccupe.
J'ai développé un système de job queue minimaliste en NodeJS avec une persistance basée sur des fichiers json simples : src/lib/server/job-queue.js.
Ma recherche avec Claude Sonnet 4 n'a révélé aucune librairie minimaliste existante qui se contente de fichiers pour la persistance.
Cette implémentation me paraît suffisamment robuste pour répondre à l'objectif que je me suis fixé.
J'ai implémenté la fonction importRevision avec nodegit pour parcourir toutes les entrées d'une révision Git du repository et les importer dans OpenSearch.
Claude Sonnet 4 m'a encore été d'une grande aide, me permettant d'éviter de passer trop de temps dans la documentation d'API de NodeGit, qui reste assez minimaliste.
Mon expérience de 2015 avec git2go sur le projet CmsHub avait été nettement plus laborieuse, à l'époque pré-LLM. Cela dit, j'avais quand même réussi. 🙂
L'implémentation du endpoint /src/routes/post_recieve_hook_url/+server.js n'a pas été très difficile.
J'ai réussi à implémenter le support de git push --force sans trop de difficulté.
Qu'est-ce qui t'a amené à choisir OpenSearch pour ce projet, plutôt qu'un autre type de base de données ?
Suite à de multiples expérimentations durant l'été 2024 (voir 2024-08-17_1253 ou Projet 5), j'ai sélectionné Elasticsearch comme moteur de base de données pour sklein-pkm-engine.
La puissance du moteur de query d'Elasticsearch m'a vraiment séduit, comme on peut le voir dans cette implémentation. Ça me paraît beaucoup plus souple que ce que j'avais développé avec postgres-tags-model-poc.
J'ai donc décidé d'explorer les possibilités d'Elasticsearch ou de son fork OpenSearch comme moteur de base de données de content repository. J'ai décidé d'en faire mon option par défaut tant que je ne rencontre pas d'obstacle majeur ou de point bloquant.
La partie où j'ai le plus hésité concerne le choix du modèle de données OpenSearch pour stocker efficacement le versioning Git.
J'ai décidé d'utiliser deux indexes distincts : files et commits :
await client.indices.create({
index: "files",
body: {
mappings: {
properties: {
content: {
type: "text"
},
mimetype: {
type: 'keyword'
},
commits: {
type: 'object',
dynamic: 'true'
}
}
}
}
});
await client.indices.create({
index: "commits",
body: {
mappings: {
properties: {
index: {
type: 'integer'
},
time: {
type: 'date',
format: 'epoch_second'
},
message: {
type: "text"
},
parents: {
type: 'keyword'
},
entries: {
type: 'object',
dynamic: 'true',
},
branches: {
type: 'keyword'
}
}
}
}
});
Après import des données depuis le repository dummy-content-repository-solar-system, voici ce qu'on trouve dans files :
[
{
_index: 'files',
_id: '2f729046cb0f02820226c1183aa04ab20ceb857d',
_score: 1,
_source: {
commits: {
'4da69e469145fe5603e57b9e22889738d066a5e2': 'mars.md',
d9bffc3da0c91366dda54fefa01383b109554054: 'mars.md'
},
mimetype: 'text/markdown; charset=utf-8'
}
},
{
_index: 'files',
_id: '1be731144f49282c43b5e7827bef986a52723a71',
_score: 1,
_source: {
commits: {
'4da69e469145fe5603e57b9e22889738d066a5e2': 'venus.md',
d9bffc3da0c91366dda54fefa01383b109554054: 'venus.md'
},
mimetype: 'text/markdown; charset=utf-8'
}
},
{
_index: 'files',
_id: 'ccc921b7a66f18e98f4887189824eefe83c7e0b3',
_score: 1,
_source: {
commits: {
'4da69e469145fe5603e57b9e22889738d066a5e2': 'terre/index.md',
a9272695d179e70cca15e89f1632b8fb76112dca: 'terre/index.md',
d9bffc3da0c91366dda54fefa01383b109554054: 'terre/index.md'
},
mimetype: 'text/markdown; charset=utf-8'
}
},
{
_index: 'files',
_id: '153d9d6e9dfedb253c624c9f25fbdb7d8691a042',
_score: 1,
_source: {
commits: {
'4da69e469145fe5603e57b9e22889738d066a5e2': 'terre/lune.md',
a9272695d179e70cca15e89f1632b8fb76112dca: 'terre/lune.md',
d9bffc3da0c91366dda54fefa01383b109554054: 'terre/lune.md'
},
mimetype: 'text/markdown; charset=utf-8'
}
},
{
_index: 'files',
_id: '97ef5b8f52f85c595bf17fac6cbec856ce80bd4a',
_score: 1,
_source: {
commits: { '4da69e469145fe5603e57b9e22889738d066a5e2': 'terre/terre.jpg' },
mimetype: 'image/jpeg'
}
}
]
et voici un exemple de contenu de commits :
[
{
_index: 'commits',
_id: '7ce2ab6f8d29fec0348342d95bfe71899dcb44fa',
_score: 1,
_source: { index: 1, time: 1757420855, branches: [ 'main' ], parents: [] }
},
{
_index: 'commits',
_id: '4da69e469145fe5603e57b9e22889738d066a5e2',
_score: 1,
_source: {
entries: {
'venus.md': {
oid: '1be731144f49282c43b5e7827bef986a52723a71',
contentType: 'text/markdown; charset=utf-8'
},
'terre/lune.md': {
oid: '153d9d6e9dfedb253c624c9f25fbdb7d8691a042',
contentType: 'text/markdown; charset=utf-8'
},
'mars.md': {
oid: '2f729046cb0f02820226c1183aa04ab20ceb857d',
contentType: 'text/markdown; charset=utf-8'
},
'terre/terre.jpg': {
oid: '97ef5b8f52f85c595bf17fac6cbec856ce80bd4a',
contentType: 'image/jpeg'
},
'terre/index.md': {
oid: 'ccc921b7a66f18e98f4887189824eefe83c7e0b3',
contentType: 'text/markdown; charset=utf-8'
}
},
index: 4,
time: 1757429173,
branches: [ 'main' ],
parents: [ 'd9bffc3da0c91366dda54fefa01383b109554054' ]
}
},
{
_index: 'commits',
_id: 'd9bffc3da0c91366dda54fefa01383b109554054',
_score: 1,
_source: {
entries: {
'venus.md': {
oid: '1be731144f49282c43b5e7827bef986a52723a71',
contentType: 'text/markdown; charset=utf-8'
},
'terre/lune.md': {
oid: '153d9d6e9dfedb253c624c9f25fbdb7d8691a042',
contentType: 'text/markdown; charset=utf-8'
},
'mars.md': {
oid: '2f729046cb0f02820226c1183aa04ab20ceb857d',
contentType: 'text/markdown; charset=utf-8'
},
'terre/index.md': {
oid: 'ccc921b7a66f18e98f4887189824eefe83c7e0b3',
contentType: 'text/markdown; charset=utf-8'
}
},
index: 3,
time: 1757421171,
branches: [ 'main' ],
parents: [ 'a9272695d179e70cca15e89f1632b8fb76112dca' ]
}
},
{
_index: 'commits',
_id: 'a9272695d179e70cca15e89f1632b8fb76112dca',
_score: 1,
_source: {
entries: {
'terre/lune.md': {
oid: '153d9d6e9dfedb253c624c9f25fbdb7d8691a042',
contentType: 'text/markdown; charset=utf-8'
},
'terre/index.md': {
oid: 'ccc921b7a66f18e98f4887189824eefe83c7e0b3',
contentType: 'text/markdown; charset=utf-8'
}
},
index: 2,
time: 1757420956,
branches: [ 'main' ],
parents: [ '7ce2ab6f8d29fec0348342d95bfe71899dcb44fa' ]
}
}
]
Ensuite, je mise beaucoup sur la puissance du moteur de requête d'OpenSearch pour récupérer efficacement les données à afficher.
Voici l'exemple de src/routes/[...pathname]/+page.server.js qui permet d'afficher le contenu d'un fichier de la branche main.
Première requête :
const responseOid = await client().search({
index: 'commits',
body: {
query: {
bool: {
must: [
{
term: {
branches: 'main'
}
},
{
exists: {
field: `entries.${params.pathname}`
}
}
]
}
},
_source: [`entries.${params.pathname}`]
}
});
Seconde requête qui utilise la réponse de la première :
const responseFile = await client().get({
index: 'files',
id: responseOid.body.hits.hits[0]._source.entries[params.pathname].oid,
_source: ['content', 'mimetype']
});
Basé sur l'expérience de ce projet, je souhaite améliorer sklein-pkm-engine pour permettre la mise à jour de notes.sklein.xyz avec mes données locales uniquement via git push, sans avoir besoin d'installer quoi que ce soit sur ma workstation.
Je pense que cette implémentation sera bien plus simple que le Projet 33, car je ne prévois pas d'inclure le support dans un premier temps. Peut-être que je supporterai les branches dans un second temps.
Journal du dimanche 24 août 2025 à 12:42
Je viens de publier la première #iteration du Projet 32 - "POC serveur Git HTTP avec exécution de scripts au push" dans le repository node-git-http-server.
L'implémentation d'un serveur Git HTTP via Apache ou nginx, en s'appuyant sur git-http-backend , paraît plutôt simple à réaliser.
Comme mon objectif est d'intégrer cette fonctionnalité dans le projet sklein-pkm-engine et que j'ai une préférence pour les monolith, j'ai exploré les solutions basées sur NodeJS.
J'ai dans un premier temps étudié le projet node-git-http-server et ensuite node-git-server.
Ces deux projets semblent peu actifs.
J'ai échoué à faire fonctionner le projet node-git-server, probablement à cause d'une erreur de ma part — j'ai sans doute oublié d'initialiser au préalable les dépôts Git en mode bare.
Par la suite, en utilisant Claude Sonnet 4, j'ai créé une implémentation basée uniquement sur les modules natifs de NodeJS et l'exécutable git-http-backend , sans recourir à aucun package NodeJS externe.
Voici le résultat : node-git-http-server/server.js.
Prochaines étapes
- Implémenter un système qui exécute du code JavaScript automatiquement après chaque
git push, en lui transmettant la branche concernée et la liste des nouveaux commits publiés. - Implémenter une déclinaison de ce projet dans un SvelteKit Custom Server.
Équivalence de l'empreinte carbone de l'entrainement de Mistral Large 2
#JaiLu cet article à propos de l'impact environnemental de Mistral Large 2 : « Notre contribution pour la création d'un standard environnemental mondial pour l'IA ».
Bien que cet article ne propose aucun lien vers le rapport complet, le fait que l'étude ait été menée en collaboration avec Carbon 4 me donne confiance. D'autant que Carbon 4 a publié un article dédié sur leur site : « Nouveau jalon dans la transparence environnementale de l'IA générative ».
Dans une note du 14 juillet 2025, j'ai écrit :
Pour Claude Sonnet 3.7 que j'ai fréquemment utilisé, je lis ceci :
- 100 in => 100 out : 0.4g
- 1k in => 1k out : 1g
- 10k in => 10k out : 2g
L'étude de Mistral AI indique un peu plus du double d'émission de CO2 pour l'inférence :
Les impacts marginaux de l'inférence, plus précisément l'utilisation de notre assistant IA Le Chat pour une réponse de 400 tokens:
- 1,14 gCO₂e
- 45 mL d'eau
- 0,16 mg de Sb eq.
1 g pour 1000 tokens versus 1,14g pour 400 tokens.
Concernant l'entrainement de Mistral Large 2, je retiens ceci :
L'empreinte environnementale de l'entraînement de Mistral Large 2 : en janvier 2025, et après 18 mois d'utilisation, Large 2 a généré les impacts suivants :
- 20,4 ktCO₂e,
- 281 000 m3 d'eau consommée, et
- 660 kg Sb eq (unité standard pour l'épuisement des ressources).
Si j'applique le référentiel de ma note du 14 juillet 2025, cette émission de CO2 lors de l'entraînement représente 115 606 trajets aller-retour Paris - Crest-Voland (Savoie) effectués avec ma voiture.
Détail du calcul : 20×1000×1000 / 173 = 115 606.
Voici une estimation grossière pour établir une comparaison.
D'après ce rapport , 8% des Français partent au ski chaque année, soit environ 5 millions de personnes (68 000 000 * 0,08 = 5 440 000).
Selon cet article BFMTV , 90% d'entre elles s'y rendent en voiture.
En supposant 4 personnes par véhicule, cela représente 1,2 million de voitures (5 440 000 * 0,9 / 4 = 1224000).
Si la moitié effectue un trajet de 500 km x 2 (aller-retour), j'obtiens 600 000 trajets.
En reprenant l'estimation d'émission de ma voiture pour cette distance, le calcul donne 600 000 * 172 kg = 103 200 000 kg, soit 130 kt de CO2, ce qui représente plus de 6 fois l'entraînement de Mistral Large 2.
Pour résumer cette Estimation de Fermi : les déplacements des parisiens vers les Alpes pour une saison de ski émettent probablement 6 fois plus de CO2 que l'entraînement de Mistral Large 2.
Dans cette note, mon but n'est pas de justifier l'intérêt de cet entraînement. Je cherchais plutôt à avoir des points de repère et des comparaisons pour mieux évaluer cet impact.
Journal du jeudi 24 juillet 2025 à 22:31
Suite à ma note "J'ai découvert ContainerLab, un projet qui permet de simuler des réseaux", j'ai implémenté et publié containerlab-playground.
Mon but était d'utiliser Containerlab pour simuler deux réseaux IPv6 connectés entre eux : 3 serveurs sur le premier réseau et 2 serveurs sur le second.
Comme je l'observe fréquemment depuis quelques mois, Claude Sonnet 4 m'a produit une implémentation qui, en pratique, ne fonctionne pas (voir son contenu dans 2025-07-20_1241).
La note courante reprend principalement en français le contenu du README.md de mon playground .
Voici les instructions que j'ai exécutées pour installer Containerlab sous Fedora :
$ sudo dnf config-manager addrepo --set=baseurl="https://netdevops.fury.site/yum/" && \
$ echo "gpgcheck=0" | sudo tee -a /etc/yum.repos.d/netdevops.fury.site_yum_.repo
$ sudo dnf install containerlab
$ sudo usermod -aG clab_admins stephane && newgrp clab_admins
$ sudo semanage fcontext -a -t textrel_shlib_t $(which containerlab)
$ sudo restorecon $(which containerlab)
Voici les instructions que j'ai exécutées pour installer Containerlab sous Fedora :
$ sudo containerlab deploy
11:27:03 INFO Containerlab started version=0.69.0
11:27:03 INFO Parsing & checking topology file=network-a-b.clab.yaml
11:27:03 INFO Creating docker network name=net-custom IPv4 subnet="" IPv6 subnet=2001:db8:a::0/48 MTU=0
11:27:03 INFO Creating lab directory path=/home/stephane/git/github.com/stephane-klein/containerlab-playground/clab-network-a
11:27:04 INFO Creating container name=vm-b1
11:27:04 INFO Creating container name=vm-a1
11:27:04 INFO Creating container name=vm-a2
11:27:04 INFO Creating container name=vm-a3
11:27:04 INFO Adding host entries path=/etc/hosts
11:27:04 INFO Adding SSH config for nodes path=/etc/ssh/ssh_config.d/clab-network-a.conf
╭──────────────────────┬───────────────────┬─────────┬─────────────────╮
│ Name │ Kind/Image │ State │ IPv4/6 Address │
├──────────────────────┼───────────────────┼─────────┼─────────────────┤
│ clab-network-a-vm-a1 │ linux │ running │ 192.168.0.2 │
│ │ mitchv85/ohv-host │ │ 2001:db8:a:1::1 │
├──────────────────────┼───────────────────┼─────────┼─────────────────┤
│ clab-network-a-vm-a2 │ linux │ running │ 192.168.0.3 │
│ │ mitchv85/ohv-host │ │ 2001:db8:a:1::2 │
├──────────────────────┼───────────────────┼─────────┼─────────────────┤
│ clab-network-a-vm-a3 │ linux │ running │ 192.168.0.4 │
│ │ mitchv85/ohv-host │ │ 2001:db8:a:1::3 │
├──────────────────────┼───────────────────┼─────────┼─────────────────┤
│ clab-network-a-vm-b1 │ linux │ running │ 192.168.0.5 │
│ │ mitchv85/ohv-host │ │ 2001:db8:a:2::5 │
╰──────────────────────┴───────────────────┴─────────┴─────────────────╯
Globalement, j'ai trouvé l'expérience utilisateur de la cli Containerlab très agréable à utiliser :
$ containerlab help
deploy container based lab environments with a user-defined interconnections
USAGE
containerlab [command] [--flags]
COMMANDS
completion [bash|zsh|fish] Generate completion script
config [command] [--flags] Configure a lab
deploy [--flags] Deploy a lab
destroy [--flags] Destroy a lab
exec [--flags] Execute a command on one or multiple containers
generate [--flags] Generate a Clos topology file, based on provided flags
graph [--flags] Generate a topology graph
help [command] [--flags] Help about any command
inspect [command] [--flags] Inspect lab details
redeploy [--flags] Destroy and redeploy a lab
save [--flags] Save containers configuration
tools [command] Various tools your lab might need
version [command] Show containerlab version or upgrade
FLAGS
-d --debug Enable debug mode
-h --help Help for containerlab
--log-level Logging level; one of [trace, debug, info, warning, error, fatal] (info)
--name Lab name
-r --runtime Container runtime
--timeout Timeout for external API requests (e.g. container runtimes), e.g: 30s, 1m, 2m30s (2m0s)
-t --topo Path to the topology definition file, a directory containing one, 'stdin', or a URL
--vars Path to the topology template variables file
-v --version Version for containerlab
J'ai eu quelques difficultés à trouver une image Docker à utiliser qui fournit directement un serveur ssh :
vm-a1:
kind: linux
# image: alpine-ssh
# https://hub.docker.com/r/mitchv85/ohv-host
image: mitchv85/ohv-host
mgmt-ipv6: 2001:db8:a:1::1
Le déploiement de cette image permet de facilement se connecter à l'host en ssh:
$ ssh admin@clab-network-a-vm-a1
admin@vm-a1:~$ exit
Il est possible de facilement lancer une commande sur tous les hosts :
$ sudo containerlab exec -t network-a-b.clab.yaml --cmd 'ip addr'
11:29:52 INFO Parsing & checking topology file=network-a-b.clab.yaml
11:29:52 INFO Executed command node=clab-network-a-vm-a2 command="ip addr"
stdout=
│ 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
│ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
│ inet 127.0.0.1/8 scope host lo
│ valid_lft forever preferred_lft forever
│ inet6 ::1/128 scope host
│ valid_lft forever preferred_lft forever
│ 2: eth0@if192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
│ link/ether aa:86:42:84:81:de brd ff:ff:ff:ff:ff:ff link-netnsid 0
│ inet 192.168.0.3/20 brd 192.168.15.255 scope global eth0
│ valid_lft forever preferred_lft forever
│ inet6 2001:db8:a:1::2/48 scope global nodad
│ valid_lft forever preferred_lft forever
│ inet6 fe80::a886:42ff:fe84:81de/64 scope link
│ valid_lft forever preferred_lft forever
11:29:52 INFO Executed command node=clab-network-a-vm-a3 command="ip addr"
stdout=
│ 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
│ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
│ inet 127.0.0.1/8 scope host lo
│ valid_lft forever preferred_lft forever
│ inet6 ::1/128 scope host
│ valid_lft forever preferred_lft forever
│ 2: eth0@if193: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
│ link/ether b2:42:6c:2b:d0:9d brd ff:ff:ff:ff:ff:ff link-netnsid 0
│ inet 192.168.0.4/20 brd 192.168.15.255 scope global eth0
│ valid_lft forever preferred_lft forever
│ inet6 2001:db8:a:1::3/48 scope global nodad
│ valid_lft forever preferred_lft forever
│ inet6 fe80::b042:6cff:fe2b:d09d/64 scope link
│ valid_lft forever preferred_lft forever
11:29:52 INFO Executed command node=clab-network-a-vm-a1 command="ip addr"
stdout=
│ 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
│ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
│ inet 127.0.0.1/8 scope host lo
│ valid_lft forever preferred_lft forever
│ inet6 ::1/128 scope host
│ valid_lft forever preferred_lft forever
│ 2: eth0@if191: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
│ link/ether 26:9f:87:52:d6:1c brd ff:ff:ff:ff:ff:ff link-netnsid 0
│ inet 192.168.0.2/20 brd 192.168.15.255 scope global eth0
│ valid_lft forever preferred_lft forever
│ inet6 2001:db8:a:1::1/48 scope global nodad
│ valid_lft forever preferred_lft forever
│ inet6 fe80::249f:87ff:fe52:d61c/64 scope link
│ valid_lft forever preferred_lft forever
11:29:52 INFO Executed command node=clab-network-a-vm-b1 command="ip addr"
stdout=
│ 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
│ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
│ inet 127.0.0.1/8 scope host lo
│ valid_lft forever preferred_lft forever
│ inet6 ::1/128 scope host
│ valid_lft forever preferred_lft forever
│ 2: eth0@if194: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
│ link/ether e2:81:7b:c7:eb:64 brd ff:ff:ff:ff:ff:ff link-netnsid 0
│ inet 192.168.0.5/20 brd 192.168.15.255 scope global eth0
│ valid_lft forever preferred_lft forever
│ inet6 2001:db8:a:2::5/48 scope global nodad
│ valid_lft forever preferred_lft forever
│ inet6 fe80::e081:7bff:fec7:eb64/64 scope link
│ valid_lft forever preferred_lft forever
Ou alors sur un host en particulier :
$ sudo containerlab exec -t network-a-b.clab.yaml --label clab-node-name=vm-a1 --cmd 'ip addr'
11:02:44 INFO Parsing & checking topology file=network-a-b.clab.yaml
11:02:44 INFO Executed command node=clab-network-a-vm-a1 command="ip addr"
stdout=
│ 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
│ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
│ inet 127.0.0.1/8 scope host lo
│ valid_lft forever preferred_lft forever
│ inet6 ::1/128 scope host
│ valid_lft forever preferred_lft forever
Par contre, je n'ai pas réussi à atteindre mon objectif 😟.
J'ai l'impression que pour le moment, Containerlab ne permet pas de créer plusieurs réseaux à partir d'un fichier topologie.
Je n'ai pas compris comment définir la longueur du prefix IPv6 des interfaces eth0 au niveau des nodes.
Pour le moment, tous les nodes appartiennent au même sous-réseau 2001:db8:a::0/48, alors que j'aimerais les séparer dans les deux sous-réseaux suivants :
2001:db8:a:12001:db8:a:2
# network-a-b.clab.yaml
...
topology:
nodes:
vm-a1:
kind: linux
# image: alpine-ssh
# https://hub.docker.com/r/mitchv85/ohv-host
image: mitchv85/ohv-host
mgmt-ipv6: 2001:db8:a:1::1
# <== ici je ne sais pas comment définir : 2001:db8:a:1::1/64
...
J'ai découvert l'issue suivante du principal développeur de Containerlab : « Multiple management networks ».
Je pense comprendre que ce que je cherche à faire n'est actuellement pas possible avec Containerlab.
Pour atteindre mon objectif, peut-être que je devrais plutôt m'orienter vers des alternatives mentionnées dans ce billet de blog : Open-source network simulation roundup 2024.
Je viens de poster le message suivant l'espace de discussion GitHub de Containerlab : « Does Containerlab support the creation of topologies with multiple subnets? ».
J'espère que le créateur de Containerlab pourra me suggérer une solution à mon problème, car je n'ai pas réussi à l'identifier dans la documentation 🤷♂️.
Voir la suite de cette note : 2025-09-19_1651.
J'utilise les LLMs comme des amis experts et jamais comme des écrivains fantômes
Un ami m'a posé la question suivante :
J'aimerais ton avis sur l'utilisation des LLM au quotidien (hors code). Les utilises-tu ? En tires-tu quelque chose de positif ? Quelles en sont les limites ?
Je vais tenter de répondre à cette question dans cette note.
Danger des LLMs : le risque de prolétarisation
Mon père et surtout mon grand-père m'ont inculqué par tradition familiale la valeur du savoir-faire. Plus tard, Bernard Stiegler m'a donné les outils théoriques pour comprendre cet enseignement à travers le concept de processus de prolétarisation.
La prolétarisation est, d’une manière générale, ce qui consiste à priver un sujet (producteur, consommateur, concepteur) de ses savoirs (savoir-faire, savoir-vivre, savoir concevoir et théoriser).
Ici, j'utilise la définition de prolétaire suivante :
Personne qui ne possède plus ses savoirs, desquels elle a été dépossédée par l’utilisation d’une technique.
En analysant mon parcours, je réalise que ma quête d'autonomie technique et de compréhension — en somme, ma recherche d'émancipation — a systématiquement guidé mes choix, comme le fait d'avoir pris le chemin du logiciel libre en 1997.
Sensibilisé à ces questions, j'ai immédiatement perçu les risques dès que j'ai découvert la puissance des LLM mi 2023 .
J'utilise les LLMs comme des amis expert d'un domaine
Les LLMs sont pour moi des pharmakons : ils sont à la fois un potentiel remède et un poison. J'essaie de rester conscient de leurs toxicités.
J'ai donc décidé d'utiliser les IA générative de texte comme je le ferais avec un ami expert d'un domaine.
Concrètement, je continue d'écrire la première version de mes notes, mails, commentaires, messages de chat ou issues sans l'aide d'IA générative de texte.
C'est seulement dans un second temps que je consulte un LLM, comme je le ferais avec un ami expert : pour lui demander un commentaire, lui poser une question ou lui demander une relecture.
J'utilise les IA générative de texte par exemple pour :
- vérifier si mon texte est explicite et compréhensible
- obtenir des suggestions d'amélioration de ma rédaction
Tout comme avec un ami, je lui partage l'intégralité de mon texte pour donner le contexte, et ensuite je lui pose des questions ciblées sur une phrase ou un paragraphe spécifique. Cette méthode me permet de mieux cadrer ses réponses.
À ce sujet, voir mes notes suivantes :
- Idée d'application de réécriture de texte assistée par IA
- Prompt - Reformulation de paragraphes pour mon notes.sklein.xyz
Par respect pour mes interlocuteurs, je ne demande jamais à un LLM de rédiger un texte à ma place.
(source)
Lorsque je trouve pertinent un contenu produit par un LLM, je le partage en tant que citation en indiquant clairement la version du modèle qui l'a généré. Je le cite comme je citerai les propos d'un humain.
En résumé, je ne m'attribue jamais les propos générés par un LLM. Je n'utilise jamais un LLM comme un écrivain fantôme.
Seconde utilisation : exploration de sujets
J'utilise aussi les LLMs pour explorer des sujets.
Je dirais que cela me permet de faire l'expérience de ce que j'appellerais "de la sérendipité dirigée".
Par exemple, je lui expose une idée et comme à un ami, je lui demande si cela a du sens pour lui, qu'est-ce que cela lui évoque et très souvent, je découvre dans ses réponses des auteurs ou des concepts que je n'ai jamais entendus parler.
J'utilise beaucoup les LLMs pour obtenir un "overview" avec une orientation très spécifique, sur des sujets tech, politique, historique…
Je l'utilise aussi souvent pour comprendre l'origine des noms des projets, ce qui me permet de mieux m'en souvenir.
Voir aussi cette note que j'ai publiée en mai 2024 : Je constate que j'utilise de plus en plus ChatGPT à la place de DuckDuckGo.
Les limites ?
En matière d'exploration, je pense que les LLMs sont d'une qualité exceptionnelle pour cette tâche. Je n'ai jamais expérimenté quelque chose d'aussi puissant. Peut-être que j'obtiendrais de meilleurs résultats en posant directement des questions à des experts mondiaux dans les domaines concernés, mais la question ne se pose pas puisque je n'ai pas accès à ces personnes.
Pour l'aide à la rédaction, il me semble que c'est nettement plus efficace que ce qu'un ami serait en mesure de proposer. Même si ce n'est pas parfait, je ne pense pas qu'un LLMs soit en mesure de deviner précisément, par lui-même, ce que j'ai l'intention d'exprimer. Il n'y a pas de magie : il faut que mes idées soient suffisamment claires dans mon cerveau pour être formulées de façon explicite. En ce qui concerne ces tâches, je constate d'importantes différences entre les modèles. Actuellement, Claude Sonnet 4 reste mon préféré pour la rédaction En revanche, j'obtiens de moins bons résultats avec les modèles chain-of-thought, ce qui est sans doute visible dans les LLM Benchmark.
Par contre, dès que je m'éloigne des questions générales pour aborder la résolution de problèmes précis, j'obtiens pour le moment des résultats très faibles. Je remarque quotidiennement des erreurs dans le domaine tech, comme :
- des paramètres inexistants
- des parties de code qui ne s'exécutent pas
- ...
Comment a évolué mon utilisation des LLMs depuis 2023 ?
J'ai publié sur https://data.sklein.xyz mes statistiques d'utilisation des LLMs de janvier 2023 à mai 2025.
Ces statistiques ne sont plus représentatives à partir de juin 2025, parce que j'ai commencé à utiliser fortement Open WebUI couplé à OpenRouter et aussi LMArena. J'aimerais prendre le temps d'intégrer les statistiques de ces plateformes prochainement.
Comme on peut le voir sur https://data.sklein.xyz, mon usage de ChatGPT a réellement démarré en avril 2024, pour évoluer vers une consommation mensuelle d'environ 300 threads.
Je suis surpris d'avoir si peu utilisé ChatGPT entre avril 2023 et janvier 2024 🤔. Je l'utilisais peut-être en mode non connecté et dans ce cas, j'ai perdu toute trace de ces interactions.
Voir aussi ma note : Estimation de l'empreinte carbone de mon usage des IA génératives de textes.
Combien je dépense en inférence LLM par mois ?
De mars à septembre 2024, 22 € par mois pour ChatGPT.
De mars à mai 2025, 22 € par mois pour Claude.ia.
Depuis juin 2025, je pense que je consomme moins de 10 € par mois, depuis que je suis passé à OpenRouter. Plus d'informations à ce sujet dans : Quelle est mon utilisation d'OpenRouter.ia ?
J'aurais encore beaucoup à dire sur le sujet des LLMs, mais j'ai décidé de m'arrêter là pour cette note.
Pour aller plus loin sur ce sujet, sous un angle très technique, je conseille cette série d'articles sur LinuxFr :
- Nouvelles sur l’IA de février 2025
- Nouvelles sur l’IA de mars 2025
- Nouvelles sur l’IA d’avril 2025
- Nouvelles sur l’IA de mai 2025
- Nouvelles sur l’IA de juin 2025
Et toutes mes notes associées au tag : #llm
Quelle est mon utilisation d'OpenRouter.ia ?
Alexandre m'a posé la question suivante :
Pourquoi utilises-tu openrouter.ai ? Quel est son intérêt principal pour toi ?
Je vais tenter de répondre à cette question dans cette note.
(Un screencast est disponible en fin de note)
Historique de mon utilisation des IA génératives payantes
Pour commencer, je pense qu’il est utile de revenir sur l’histoire de mon usage des IA génératives de texte payantes, afin de mieux comprendre ce qui m’a amené à utiliser openrouter.ai.
En juin 2023, j'ai expérimenté l'API ChatGPT dans ce POC poc-api-gpt-generate-demo-datas et je me rappelle avoir brûlé mes 10 € de crédit très rapidement.
Cette expérience m'a mené à la conclusion que pour utiliser des LLM dans le futur, je devrais passer par du self-hosting.
C'est pour cela que je me suis fortement intéressé à Llama.cpp en 2024, comme l'illustrent ces notes :
- 2024 janvier : J'ai lu le README.md de Ollama
- 2024 mai : Je me demande combien me coûterait l'hébergement de Lllama.cpp sur une GPU instance de Scaleway
- 2024 mai : Lecture active de l'article « LLM auto-hébergés ou non : mon expérience » de LinuxFr
- 2024 juin : Déjeuner avec un ami sur le thème, auto-hébergement de LLMs
J'ai souscrit à ChatGPT Plus pour environ 22 € par mois de mars à septembre 2024.

Je pensais que cette offre était probablement bien plus économique que l'utilisation directe de l'API ChatGPT. Avec du recul, je pense que ce n'était pas le cas.
Après avoir lu plusieurs articles sur Anthropic — notamment la section Historique de l'article Wikipédia — et constaté les retours positifs sur Claude Sonnet (voir la note 2025-01-12_1509), j’ai décidé de tester Claude.ia pendant un certain temps.
Le 3 mars 2025, je me suis abonné à l'offre Claude Pro à 21,60 € par mois.

Durant cette même période, j'ai utilisé avante.nvim connecté à Claude Sonnet via le provider Copilot, voir note : J'ai réussi à configurer Avante.nvim connecté à Claude Sonnet via le provider Copilot.
En revanche, comme je l’indique ici , je n’ai jamais réussi à trouver, dans l’interface web de GitHub, mes statistiques d’utilisation ni les quotas associés à Copilot. J’avais en permanence la crainte de découvrir un jour une facture salée.
Au mois d'avril 2025, j'ai commencé à utiliser Scaleway Generative APIs connecté à Open WebUI : voir note 2025-04-25_1833.
Pour résumer, ma situation en mai 2025 était la suivante
- Je pensais que l'utilisation des API directes d'OpenAI ou d'Anthropic était hors de prix.
- Je payais un abonnement mensuel d'un peu plus de 20 € pour un accès à Claude.ia via leur agent conversationnel web
- Je commençais à utiliser Scaleway Generative APIs avec accès à un nombre restreint de modèles
- Étant donné que je souscrivais à un abonnement, je ne pouvais pas facilement passer d'un provider à un autre. Quand je décidais de prendre un abonnement Claude.ia alors j'arrêtais d'utiliser ChatGPT.
En mai 2025, j'ai commencé sans conviction à m'intéresser à OpenRouter
J'ai réellement pris le temps de tester OpenRouter le 30 mai 2025. J'avais déjà croisé ce projet plusieurs fois auparavant, probablement dans la documentation de Aider, llm (cli) et sans doute sur le Subreddit LocalLLaMa.
Avant de prendre réellement le temps de le tester, en ligne de commande et avec Open WebUI, je n'avais pas réellement compris son intérêt.
Je ne comprenais pas l'intérêt de payer 5% de frais supplémentaires à openrouter.ai pour accéder aux modèles payants d'OpenAI ou Anthropic 🤔 !
Au même moment, je m'interrogeais sur les limites de quotas de tokens de l'offre Claude Pro.
For Individual Power Users: Claude Pro Plan
- All Free plan features.
- Approximately 5 times more usage than the Free plan.
- ...
J'étais très surpris de constater que la documentation de l'offre Claude Pro , contrairement à celle de l'API, ne précisait aucun chiffre concernant les limites de consommation de tokens.
Même constat du côté de ChatGPT :
ChatGPT Plus
- Toutes les fonctionnalités de l’offre gratuite
- Limites étendues sur l’envoi de messages, le chargement de fichiers, l’analyse de données et la génération d’images
- ...
Je me souviens d'avoir effectué diverses recherches sur Reddit à ce sujet, mais sans succès.
J'ai interrogé Claude.ia et il m'a répondu ceci :
L'offre Claude Pro vous donne accès à environ 3 millions de tokens par mois. Ce quota est remis à zéro chaque mois et vous permet d'utiliser Claude de manière plus intensive qu'avec le plan gratuit.
Aucune précision n'est donnée concernant une éventuelle répartition des tokens d'input et d'output, pas plus que sur le modèle LLM qui est sélectionné.
J'ai fait ces petits calculs de coûts sur llm-prices :
- En prenant l'hypothèse de 1 million de tokens en entrée et 2 millions en sortie :
- Le modèle Claude Sonnet 4 coûterait environ
$33. - Le modèle Claude Haiku coûterait environ
$2,75.
- Le modèle Claude Sonnet 4 coûterait environ
J'en ai déduit que le prix des abonnements n'est peut-être pas aussi économique que je le pensais initialement.
Après cela, j'ai calculé le coût de plusieurs de mes discussions sur Claude.ia. J'ai été surpris de voir que les prix étaient bien inférieurs à ce que je pensais : seulement 0,003 € pour une petite question, et environ 0,08 € pour générer un texte de 5000 mots.
J'ai alors pris la décision de tester openrouter.ai avec 10 € de crédit. Je me suis dit : "Au pire, si openrouter.ai est inutile, je perdrai seulement 0,5 €".
Je pensais que je n'avais pas à me poser de questions tant qu'openrouter.ai ne me coûtait qu'un ou deux euros par mois.
Suite à cette décision, j'ai commencé à utiliser openrouter.ai avec Open WebUI en utilisant ce playground : open-webui-deployment-playground.
Ensuite, je me suis lancé dans « Projet 30 - "Setup une instance personnelle d'Open WebUI connectée à OpenRouter" » pour héberger cela un peu plus proprement.
Et dernièrement, j'ai connecté avante.nvim à OpenRouter : Switch from Copilot to OpenRouter with Gemini 2.0 Flash for Avante.nvim.
Après plus d'un mois d'utilisation, voici ce que OpenRouter m'apporte
Entre le 30 mai et le 15 juillet 2025, j'ai consommé $14,94 de crédit. Ce qui est moindre que l'abonnement de 22 € par mois de Claude Pro.

D'après mes calculs basés sur https://data.sklein.xyz, en utilisant OpenRouter j'aurais dépensé :
- mars 2025 :
$3.07 - avril 2025 :
$2,76 - mai 2025 :
$2,32
Ici aussi, ces montants sont bien moindres que les 22 € de l'abonnement Claude Pro.
En utilisant OpenRouter, j'ai accès facilement à plus de 400 instances de models, dont la plupart des modèles propriétaires, comme ceux de OpenAI, Claude, Gemini, Mistral AI…
Je n'ai plus à me poser la question de prendre un abonnement chez un provider ou un autre.
Je dépose simplement des crédits sur openrouter.ai et après, je suis libre d'utiliser ce que je veux.
openrouter.ai me donne l'opportunité de tester différents modèles avec plus de liberté.
J'ai aussi accès à énormément de modèles gratuitement, à condition d'accepter que ces providers exploitent mes prompts pour de l'entrainement. Plus de détail ici : Privacy, Logging, and Data Collection.
Tout ceci est configurable dans l'interface web de OpenRouter :

Je peux générer autant de clés d'API que je le désire. Et ce que j'apprécie particulièrement, c'est la possibilité de paramétrer des quotas de crédits spécifiques pour chaque clé ❤️.

OpenRouter me donne bien entendu accès aux fonctionnalités avancées des modèles, par exemple Structured Outputs with LLM, ou "tools" :

J'ai aussi accès à un dashboard d'activité, je peux suivre avec précision mes consommations :

Je peux aussi utiliser OpenRouter dans mes applications, avec llm (cli), avante.nvim… Je n'ai plus à me poser de question.
Et voici un petit screencast de présentation de openrouter.ai :